//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.IO;
using System.Collections;
using EffiProzDB.RowIO;
using EffiProzDB.Lib;

namespace EffiProzDB.Persist
{

    /**
     * Acts as a manager for CACHED table persistence.<p>
     *
     * This contains the top level functionality. Provides file management services
     * and access.<p>
     *
     * Rewritten for 1.8.0 together with Cache.
     *
     * @author fredt@users
     * @version 1.8.0
     * @since 1.7.2
     */
    public class DataFileCache
    {

        protected HSQLFileAccess fa;

        // flags
        public const int FLAG_ISSAVED = 2;
        public const int FLAG_ROWINFO = 3;

        // file format fields
        public const int LONG_EMPTY_SIZE = 4;     // empty space size
        public const int LONG_FREE_POS_POS = 12;    // where iFreePos is saved
        public const int LONG_EMPTY_INDEX_POS = 20;    // empty space index
        public const int FLAGS_POS = 28;
        public const int INITIAL_FREE_POS = 32;

        //
        public DataFileBlockManager freeBlocks;
        private const int initIOBufferSize = 256;

        //
        protected string fileName;
        protected string backupFileName;
        protected Database database;

        // this flag is used externally to determine if a backup is required
        public bool fileModified;
        public int cacheFileScale;

        // post openning constant fields
        public bool cacheReadonly;

        // cache operation mode
        protected bool storeOnInsert;

        //
        protected int cachedRowPadding = 8;
        protected bool _hasRowInfo = false;

        // reusable input / output streams
        protected RowInputInterface rowIn;
        protected RowOutputInterface rowOut;

        //
        public long maxDataFileSize;

        //
        protected Storage dataFile;
        protected long fileFreePosition;
        protected int maxCacheSize;                // number of Rows
        protected long maxCacheBytes;               // number of bytes
        protected int maxFreeBlocks;
        protected Cache cache;

        public DataFileCache(Database db,
                             string baseFileName)
        {

            initParams(db, baseFileName);

            cache = new Cache(this);
        }

        /**
         * initial external parameters are set here.
         */
        protected virtual void initParams(Database database,
                                  string baseFileName)
        {

            HsqlDatabaseProperties props = database.getProperties();

            fileName = baseFileName + ".data";
            backupFileName = baseFileName + ".backup";
            this.database = database;
            fa = database.getFileAccess();

            int cacheScale =
                props.getIntegerProperty(HsqlDatabaseProperties.hsqldb_cache_scale,
                                         14, 8, 18);
            int cacheSizeScale = props.getIntegerProperty(
                HsqlDatabaseProperties.hsqldb_cache_size_scale, 10, 6, 20);
            int cacheFreeCountScale = props.getIntegerProperty(
                HsqlDatabaseProperties.hsqldb_cache_free_count_scale, 9, 6, 12);

            cacheFileScale = database.getProperties().getIntegerProperty(
                HsqlDatabaseProperties.hsqldb_cache_file_scale, 1);

            if (cacheFileScale != 1)
            {
                cacheFileScale = 8;
            }

            cacheReadonly = database.isFilesReadOnly();

            int lookupTableLength = 1 << cacheScale;
            int avgRowBytes = 1 << cacheSizeScale;

            maxCacheSize = lookupTableLength * 3;
            maxCacheBytes = maxCacheSize * avgRowBytes;
            maxDataFileSize = cacheFileScale == 1 ? int.MaxValue
                                                  : (long)int.MaxValue * 4;
            maxFreeBlocks = 1 << cacheFreeCountScale;
            dataFile = null;
        }

        /**
         * Opens the *.data file for this cache, setting the variables that
         * allow access to the particular database version of the *.data file.
         */
        public virtual void open(bool _readonly)
        {

            fileFreePosition = 0;

            database.logger.appLog.logContext(SimpleLog.LOG_NORMAL, "start");

            try
            {
                bool preexists =false;
                long freesize = 0;

                if (fa.IsStreamElement(fileName))
                {

                    // OOo related code
                    if (database.isStoredFileAccess())
                    {
                        preexists = true;
                    }

                    // OOo end;
                    if (!preexists)
                    {

                        // discard "empty" databases
                        FileInfo f = new FileInfo(fileName);

                        preexists = f.Length > INITIAL_FREE_POS;
                    }
                }

                if (preexists)
                {
                    string version = database.getProperties().getProperty(
                        HsqlDatabaseProperties.hsqldb_cache_version);
                    bool v17 =
                        HsqlDatabaseProperties.VERSION_STRING_1_7_0.Equals(
                            version);

                    // for later versions
                    bool v18 =
                        HsqlDatabaseProperties.VERSION_STRING_1_8_0.Equals(
                            version);

                    if (!v17)
                    {
                        throw Trace.error(Trace.WRONG_DATABASE_FILE_VERSION);
                    }
                }

                bool isNio = database.getProperties().isPropertyTrue(
                    HsqlDatabaseProperties.hsqldb_nio_data_file);
                int fileType = isNio ? ScaledRAFile.DATA_FILE_NIO
                                     : ScaledRAFile.DATA_FILE_RAF;
             

               

                dataFile = ScaledRAFile.newScaledRAFile(database, fileName,
                        _readonly, fileType);

                if (preexists)
                {
                    dataFile.seek(FLAGS_POS);

                    int flags = dataFile.readInt();

                    _hasRowInfo = BitMap.isSet(flags, FLAG_ROWINFO);

                    dataFile.seek(LONG_EMPTY_SIZE);

                    freesize = dataFile.readLong();

                    dataFile.seek(LONG_FREE_POS_POS);

                    fileFreePosition = dataFile.readLong();

                    if (fileFreePosition < INITIAL_FREE_POS)
                    {
                        fileFreePosition = INITIAL_FREE_POS;
                    }
                }
                else
                {
                    fileFreePosition = INITIAL_FREE_POS;

                    dataFile.seek(LONG_FREE_POS_POS);
                    dataFile.writeLong(INITIAL_FREE_POS);

                    // set unsaved flag;
                    dataFile.seek(FLAGS_POS);
                    dataFile.writeInt(0);
                }

                initBuffers();

                fileModified = false;
                freeBlocks = new DataFileBlockManager(maxFreeBlocks,
                                                      cacheFileScale, freesize);

                database.logger.appLog.logContext(SimpleLog.LOG_NORMAL, "end");
            }
            catch (Exception e)
            {
                database.logger.appLog.logContext(e, "failed");
                close(false);

                throw Trace.error(Trace.FILE_IO_ERROR, Trace.DataFileCache_open,
                                  new Object[] {
                e, fileName
            });
            }
        }

        /**
         *  Parameter write indicates either an orderly close, or a fast close
         *  without backup.
         *
         *  When false, just closes the file.
         *
         *  When true, writes _out all cached rows that have been modified and the
         *  free position pointer for the *.data file and then closes the file.
         */
        public virtual void close(bool write)
        {

            SimpleLog appLog = database.logger.appLog;

            try
            {
                if (cacheReadonly)
                {
                    if (dataFile != null)
                    {
                        dataFile.close();
                    }

                    return;
                }

                StopWatch sw = new StopWatch();

                appLog.sendLine(SimpleLog.LOG_NORMAL,
                                "DataFileCache.close(" + write + ") : start");

                if (write)
                {
                    cache.saveAll();
                    Trace.printSystemOut("saveAll: " + sw.elapsedTime());
                    appLog.sendLine(SimpleLog.LOG_NORMAL,
                                    "DataFileCache.close() : save data");

                    if (fileModified || freeBlocks.isModified())
                    {

                        // set empty
                        dataFile.seek(LONG_EMPTY_SIZE);
                        dataFile.writeLong(freeBlocks.getLostBlocksSize());

                        // set end
                        dataFile.seek(LONG_FREE_POS_POS);
                        dataFile.writeLong(fileFreePosition);

                        // set saved flag;
                        dataFile.seek(FLAGS_POS);

                        int flag = BitMap.set(0, FLAG_ISSAVED);

                        if (_hasRowInfo)
                        {
                            flag = BitMap.set(flag, FLAG_ROWINFO);
                        }

                        dataFile.writeInt(flag);
                        appLog.sendLine(SimpleLog.LOG_NORMAL,
                                        "DataFileCache.close() : flags");

                        //
                        if (dataFile.length() != fileFreePosition)
                        {
                            dataFile.seek(fileFreePosition);
                        }

                        appLog.sendLine(SimpleLog.LOG_NORMAL,
                                        "DataFileCache.close() : seek end");
                        Trace.printSystemOut("pos and flags: " + sw.elapsedTime());
                    }
                }

                if (dataFile != null)
                {
                    dataFile.close();
                    appLog.sendLine(SimpleLog.LOG_NORMAL,
                                    "DataFileCache.close() : close");

                    dataFile = null;

                    Trace.printSystemOut("close: " + sw.elapsedTime());
                }

                bool empty = fileFreePosition == INITIAL_FREE_POS;

                if (empty)
                {
                    fa.RemoveElement(fileName);
                    fa.RemoveElement(backupFileName);
                }
            }
            catch (Exception e)
            {
                appLog.logContext(e, null);

                throw Trace.error(Trace.FILE_IO_ERROR, Trace.DataFileCache_close,
                                  new Object[] {
                e, fileName
            });
            }
          
        }

        protected virtual void initBuffers()
        {

            if (rowOut == null
                    || ((RowOutputBinary)rowOut).getBuffer().Length
                       > initIOBufferSize)
            {
                rowOut = new RowOutputBinary(256);
            }

            if (rowIn == null
                    || ((RowInputBinary)rowIn).getBuffer().Length
                       > initIOBufferSize)
            {
                rowIn = new RowInputBinary(new byte[256]);
            }
        }

        /**
         *  Writes _out all the rows to a new file without fragmentation.
         */
        public void defrag()
        {

            if (cacheReadonly)
            {
                return;
            }

            if (fileFreePosition == INITIAL_FREE_POS)
            {
                return;
            }

            database.logger.appLog.logContext(SimpleLog.LOG_NORMAL, "start");

            try
            {
                bool wasNio = dataFile.wasNio();

                cache.saveAll();

                DataFileDefrag dfd = new DataFileDefrag(database, this, fileName);

                dfd.process();
                close(false);
                deleteFile(wasNio);
                renameDataFile();
                backupFile();
                database.getProperties().setProperty(
                    HsqlDatabaseProperties.hsqldb_cache_version,
                    HsqlDatabaseProperties.THIS_CACHE_VERSION);
                database.getProperties().save();
                cache.clear();

                cache = new Cache(this);

                open(cacheReadonly);
                dfd.updateTableIndexRoots();
                dfd.updateTransactionRowIDs();
            }
            catch (Exception e)
            {
                database.logger.appLog.logContext(e, null);

                throw new HsqlException(
                    e, Trace.getMessage(Trace.GENERAL_IO_ERROR),
                    Trace.GENERAL_IO_ERROR);
            }

            database.logger.appLog.logContext(SimpleLog.LOG_NORMAL, "end");
        }

        /**
         * Used when a row is deleted as a result of some DML or DDL command.
         * Removes the row from the cache data structures.
         * Adds the file space for the row to the list of free positions.
         */
        public virtual void remove(int i,
                                        PersistentStore store)
        {
            lock (this)
            {
                CachedObject r = release(i);

                // bug fix for 1.8.0.10
                // if r is not _in the cache, it is not known if the space has already
                // been reallocated to multiple rows, therefore we no longer read the
                // space size from file
                if (r != null)
                {
                    int size = r.getStorageSize();

                    freeBlocks.add(i, size);
                }
            }
        }

        public virtual void removePersistence(int i,
                PersistentStore store)
        {

            lock (this)
            {
            }
        }

        /**
         * Allocates file space for the row. <p>
         *
         * Free space is requested from the block manager if it exists.
         * Otherwise the file is grown to accommodate it.
         */
        private int setFilePos(CachedObject r)
        {
            lock (this)
            {
                int rowSize = r.getStorageSize();
                int i = freeBlocks == null ? -1
                                                 : freeBlocks.get(rowSize);

                if (i == -1)
                {
                    i = (int)(fileFreePosition / cacheFileScale);

                    long newFreePosition = fileFreePosition + rowSize;

                    if (newFreePosition > maxDataFileSize)
                    {
                        throw new IOException(
                            Trace.getMessage(Trace.DATA_FILE_IS_FULL));
                    }

                    fileFreePosition = newFreePosition;
                }

                r.setPos(i);

                return i;
            }
        }

        public virtual void add(CachedObject _object)
        {
            lock (this)
            {
                int size = _object.getRealSize(rowOut);

                size = ((size + cachedRowPadding - 1) / cachedRowPadding)
                       * cachedRowPadding;

                _object.setStorageSize(size);

                int i = setFilePos(_object);

                cache.put(i, _object);

                // was previously used for text tables
                if (storeOnInsert)
                {
                    saveRow(_object);
                }
            }
        }

        /**
         * For a CacheObject that had been previously released from the cache.
         * A new version is introduced, using the preallocated space for the object.
         */
        public void restore(CachedObject _object)
        {
            lock (this)
            {
                int i = _object.getPos();

                cache.put(i, _object);

                // was previously used for text tables
                if (storeOnInsert)
                {
                    saveRow(_object);
                }
            }
        }

        public int getStorageSize(int i)
        {
            lock (this)
            {
                CachedObject value = cache.get(i);

                if (value != null)
                {
                    return value.getStorageSize();
                }

                return readSize(i);
            }
        }

        public virtual CachedObject get(int i, PersistentStore store,
                                             bool keep)
        {
            lock (this)
            {

                if (i < 0)
                {
                    return null;
                }

                try
                {
                    CachedObject _object = cache.get(i);

                    if (_object == null)
                    {
                        RowInputInterface rowInput = readObject(i);

                        if (rowInput == null)
                        {
                            return null;
                        }

                        _object = store.get(rowInput);

                        if (_object == null)
                        {
                            throw new IOException("cannot build object from file");
                        }

                        // for text tables with empty rows at the beginning,
                        // pos may move forward _in readObject
                        i = _object.getPos();

                        cache.put(i, _object);
                    }

                    if (keep)
                    {
                        _object.keepInMemory(true);
                    }

                    return _object;
                }
                catch (IOException e)
                {
                    database.logger.appLog.logContext(e, fileName + " get pos: " + i);

                    throw Trace.error(Trace.DATA_FILE_ERROR,
                                      Trace.DataFileCache_makeRow, new Object[] {
                e, fileName
            });
                }

            }
        }

        public RowInputInterface getRaw(int i)
        {
            lock (this)
            {
                return readObject(i);
            }
        }

        public int readSize(int pos)
        {
            lock (this)
            {
                dataFile.seek((long)pos * cacheFileScale);

                return dataFile.readInt();
            }
        }

        public virtual RowInputInterface readObject(int pos)
        {
            lock (this)
            {
                dataFile.seek((long)pos * cacheFileScale);

                int size = dataFile.readInt();

                rowIn.resetRow(pos, size);
                dataFile.read(rowIn.getBuffer(), 4, size - 4);

                return rowIn;
            }
        }

        public CachedObject release(int i)
        {
            lock (this)
            {
                return cache.release(i);
            }
        }

        public virtual void saveRows(CachedObject[] rows, int offset,
         int count)
        {
            lock (this)
            {
                if (count == 0)
                {
                    return;
                }

                try
                {
                    for (int i = offset; i < offset + count; i++)
                    {
                        CachedObject r = rows[i];

                        saveRow(r);

                        rows[i] = null;
                    }
                }
                catch (IOException e)
                {
                    database.logger.appLog.logContext(e, null);

                    throw e;
                }
                catch (Exception e)
                {
                    database.logger.appLog.logContext(e, null);

                    throw new IOException(e.ToString());
                }
                finally
                {
                    initBuffers();
                }
            }
        }

        /**
         * Writes _out the specified Row. Will write only the Nodes or both Nodes
         * and table row data depending on what is not already persisted to disk.
         */
        public virtual void saveRow(CachedObject row)
        {
            lock (this)
            {
                setFileModified();
                rowOut.reset();
                row.write(rowOut);
                dataFile.seek((long)row.getPos() * cacheFileScale);
                dataFile.write(rowOut.getOutputStream().getBuffer(), 0,
                               rowOut.getOutputStream().size());
            }
        }

        /**
         *  Saves the *.data file as compressed *.backup.
         *
         * @throws  HsqlException
         */
        public void backupFile()
        {

            try
            {
                if (fa.IsStreamElement(fileName))
                {
                    ZipUnzipFile.compressFile(fileName, backupFileName + ".new",
                                              database.getFileAccess());
                }
            }
            catch (IOException e)
            {
                database.logger.appLog.logContext(e, null);

                throw e;
            }
        }

        public void renameBackupFile()
        {

            if (fa.IsStreamElement(backupFileName + ".new"))
            {
                fa.RemoveElement(backupFileName);
                fa.RenameElement(backupFileName + ".new", backupFileName);
            }
        }

        /**
         *  Renames the *.data.new file.
         *
         * @throws  HsqlException
         */
        public void renameDataFile()
        {

            if (fa.IsStreamElement(fileName + ".new"))
            {
                fa.RemoveElement(fileName);
                fa.RenameElement(fileName + ".new", fileName);
            }
        }

        public void deleteFile(bool wasNio)
        {

            // first attemp to delete
            fa.RemoveElement(fileName);

            // OOo related code
            if (database.isStoredFileAccess())
            {
                return;
            }

            // OOo end
            if (fa.IsStreamElement(fileName))
            {
                if (wasNio)
                {
                    //System.gc();
                    fa.RemoveElement(fileName);
                }

                if (fa.IsStreamElement(fileName))
                {
                    fa.RenameElement(fileName, fileName + ".old");

                    FileInfo oldfile = new FileInfo(fileName + ".old");

                    FileUtil.GetDefaultInstance().deleteOnExit(oldfile);
                }
            }
        }

        public void deleteBackup()
        {
            fa.RemoveElement(backupFileName);
        }

        /**
         * This method deletes a data file or resets its free position.
         * this is used only for nio files - not OOo files
         */
        public static void deleteOrResetFreePos(Database database, string filename)
        {

            ScaledRAFile raFile = null;

            database.getFileAccess().RemoveElement(filename);

            // OOo related code
            if (database.isStoredFileAccess())
            {
                return;
            }

            // OOo end
            if (!database.getFileAccess().IsStreamElement(filename))
            {
                return;
            }

            try
            {
                raFile = new ScaledRAFile(database, filename, false);

                raFile.seek(LONG_FREE_POS_POS);
                raFile.writeLong(INITIAL_FREE_POS);
            }
            catch (IOException e)
            {
                database.logger.appLog.logContext(e, null);
            }
            finally
            {
                if (raFile != null)
                {
                    try
                    {
                        raFile.close();
                    }
                    catch (IOException e)
                    {
                        database.logger.appLog.logContext(e, null);
                    }
                }
            }
        }

        public int capacity()
        {
            return maxCacheSize;
        }

        public long bytesCapacity()
        {
            return maxCacheBytes;
        }

        public long getTotalCachedBlockSize()
        {
            return cache.getTotalCachedBlockSize();
        }

        public int getFreeBlockCount()
        {
            return freeBlocks.size();
        }

        public int getTotalFreeBlockSize()
        {
            return 0;
        }

        public long getFileFreePos()
        {
            return fileFreePosition;
        }

        public int getCachedObjectCount()
        {
            return cache.size();
        }

        public string getFileName()
        {
            return fileName;
        }

        public bool hasRowInfo()
        {
            return _hasRowInfo;
        }

        public bool isFileModified()
        {
            return fileModified;
        }

        protected virtual void setFileModified()
        {
            lock (this)
            {
                if (!fileModified)
                {
                    /*
                                // unset saved flag;
                                dataFile.seek(FLAGS_POS);

                                int flag = BitMap.set(0, FLAG_ISSAVED);

                                if (hasRowInfo) {
                                    flag = BitMap.set(flag, FLAG_ROWINFO);
                                }

                                dataFile.writeInt(flag);
                    */
                    fileModified = true;
                }
            }
        }

    }
}
